def reset(fast_check=true)
@parser.reset
@read_header = true
- @read_proxy = !!@expect_proxy_proto
+ @read_proxy = !!@expect_proxy_proto && @requests_served.zero?
@env = @proto_env.dup
@body = nil
@tempfile = nil
def try_to_parse_proxy_protocol
if @read_proxy
if @expect_proxy_proto == :v1
- if @buffer.include? "\r\n"
- if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
- if md[1]
- @peerip = md[1].split(" ")[0]
+ crlf_index = @buffer.index "\r\n"
+
+ unless crlf_index
+ if "PROXY ".start_with? @buffer
+ return false
+ elsif @buffer.start_with? "PROXY "
+ if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
+ raise ConnectionError, "PROXY protocol v1 line is too long"
end
- @buffer = md.post_match
+ return false
end
- # if the buffer has a \r\n but doesn't have a PROXY protocol
- # request, this is just HTTP from a non-PROXY client; move on
+
@read_proxy = false
- return @buffer.size > 0
- else
- return false
+ return true
+ end
+
+ if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
+ raise ConnectionError, "PROXY protocol v1 line is too long"
+ end
+
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
+ if md[1]
+ @peerip = md[1].split(" ")[0]
+ end
+ @buffer = md.post_match
end
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
+ # request, this is just HTTP from a non-PROXY client; move on
+ @read_proxy = false
+ return @buffer.size > 0
end
end
true
assert_equal 'fd00::1', remote_addr
end
+ def test_proxy_protocol_rejects_line_without_crlf_at_max_length
+ server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do
+ [200, {}, ["Hello"]]
+ end
+
+ socket = new_connection
+ socket << "PROXY #{'A' * (Puma::Const::PROXY_PROTOCOL_V1_MAX_LENGTH - "PROXY ".bytesize)}"
+
+ assert_equal "", socket.read
+ end
+
+ def test_proxy_protocol_allows_non_proxy_requests_over_max_length
+ server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do
+ [200, {}, ["Hello"]]
+ end
+
+ response = send_http_and_read( "GET /#{'a' * Puma::Const::PROXY_PROTOCOL_V1_MAX_LENGTH} HTTP/1.0\r\n\r\n")
+
+ assert_match %r{\AHTTP/1\.[01] 200 OK}, response
+ assert_match %r{Hello\z}, response
+ end
+
+ def test_proxy_protocol_ignores_embedded_proxy_line_in_http_body
+ server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env|
+ body = env['rack.input'].read
+ [200, {'Content-Length' => body.bytesize.to_s}, [body]]
+ end
+
+ body = "prefix\nPROXY TCP4 1.1.1.1 2.2.2.2 3 4\r\nsuffix"
+ response = send_http_and_read ("POST / HTTP/1.1\r\nHost: test.com\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}")
+
+ assert_match %r{\AHTTP/1\.[01] 200 OK}, response
+ assert_match %r{#{Regexp.escape(body)}\z}, response
+ end
+
+ def test_proxy_protocol_only_parses_first_request_on_connection
+ server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env|
+ [200, {}, [env["REMOTE_ADDR"]]]
+ end
+
+ socket = send_proxy_v1_http("GET /one HTTP/1.1\r\nHost: test.com\r\n\r\n", "1.2.3.4")
+
+ assert_equal ["HTTP/1.1 200 OK", "Content-Length: 7"], header(socket)
+ assert_equal "1.2.3.4", socket.read(7)
+
+ sleep 1
+
+ socket << "PROXY TCP4 127.0.0.1 127.0.0.1 10000 80\r\nGET /two HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n"
+
+ assert_match %r{\AHTTP/1\.[01] 400 Bad Request}, socket.read
+ end
+
+ def test_proxy_protocol_allows_proxy_http_method_on_keep_alive
+ server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1, supported_http_methods: :any) do |env|
+ [200, {}, [env["REMOTE_ADDR"]]]
+ end
+
+ socket = send_proxy_v1_http("GET /one HTTP/1.1\r\nHost: test.com\r\n\r\n", "1.2.3.4")
+
+ assert_equal ["HTTP/1.1 200 OK", "Content-Length: 7"], header(socket)
+ assert_equal "1.2.3.4", socket.read(7)
+
+ socket << "PROXY /two HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n"
+
+ response = socket.read
+ assert_match %r{\AHTTP/1\.1 200 OK\r\n}, response
+ assert_equal "1.2.3.4", response.split("\r\n").last
+ end
+
# To comply with the Rack spec, we have to split header field values
# containing newlines into multiple headers.
def assert_does_not_allow_http_injection(app, opts = {})